package com.wyw.elasticsearch.learning.controller;

import com.wyw.elasticsearch.learning.common.Tes123;
import com.wyw.elasticsearch.learning.common.Tes12345;
import com.wyw.elasticsearch.learning.common.bean.Stu;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.common.unit.Fuzziness;
import org.elasticsearch.index.query.QueryBuilders;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.*;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

/**
 * @Title
 * @Description
 * @Author Mr Wu yewen.wu.china@gmail.com
 * @Date 2024/4/8 16:19
 * Update History:
 * Author        Time            Content
 */
@RestController
@RequestMapping("es")
@Slf4j
@RequiredArgsConstructor
public class EsTestController {

    private final ElasticsearchRestTemplate esTemplate;

    // 一般来说为了规范，我们都是通过手动来创建索引的，因为索引一旦创建就无法修改，除非删除，连field的类型是什么也不能修改，所以往往都是在初期规划好的。
    /**
     * 创建索引（相当于创建表）
     */
    @GetMapping("createIndex")
    public Object createIndex() {
        esTemplate.indexOps(Stu.class).create();
        return "OK";
    }

    /**
     * 删除索引（相当于删除表）
     */
    @GetMapping("deleteIndex")
    public Object deleteIndex() {
        esTemplate.indexOps(Stu.class).delete();
        return "OK";
    }

    /*
        POST        /stu/_doc/_mapping?include_type_name=true
{
    "properties": {
        "stuId": {
            "type": "long"
        },
        "name": {
            "type": "text",
            "analyzer": "ik_max_word"
        },
        "age": {
            "type": "integer"
        },
        "money": {
            "type": "float"
        },
        "desc": {
            "type": "text",
            "analyzer": "ik_max_word"
        }
    }
}

     */

    /**
     * 新增文档
     */
    @GetMapping("save")
    public Object save() {
        Stu stu = new Stu();
        stu.setStuId(1000L);
        stu.setName("wyw-学习-A");
        stu.setAge(29);
        stu.setMoney(500.8f);
        stu.setDescription("学习elasticsearch~~");

        IndexQuery iq = new IndexQueryBuilder().withObject(stu).build();

        IndexCoordinates ic = IndexCoordinates.of("stu");

        String saveResult = esTemplate.index(iq, ic);
        log.info("新增返回:[{}]", saveResult);
        return "OK";
    }

    /**
     * 更新文档
     */
    @GetMapping("update")
    public Object update() {

        HashMap<String, Object> map = new HashMap<>();
        map.put("name", "学习a");
        map.put("age", 22);
        map.put("money", 600.6f);
        map.put("description", "学习elasticsearch7~~");

        UpdateQuery uq = UpdateQuery.builder("1000")
                .withDocument(Document.from(map))
                .build();

        UpdateResponse updateResult = esTemplate.update(uq, IndexCoordinates.of("stu"));
        log.info("更新返回:[{}]", updateResult);
        return "OK";
    }

    /**
     * 查询文档
     *
     *  测试数据在pic下     pic/查询学习测试数据.jpg
     *
     *  QueryBuilders是ES中的查询条件构造器
     *
     *  精确查询
     *      指定字符串作为关键词查询，关键词支持分词
     *          查询title字段中，包含“开发”，“开放”这个字符串的document；相当于把“浦东开发开放”分词了，再查询；
     *          QueryBuilders.queryStringQuery("开发开放").defaultField("title");
     *          不指定feild，查询范围为所有feild
     *          QueryBuilders.queryStringQuery("青春");
     *          指定多个feild
     *          QueryBuilders.queryStringQuery("青春").field("title").field("content");
     *      以关键字"开发开放"，关键字不支持分词
     *          QueryBuilders.termQuery("title", "开发开放");
     *          QueryBuilders.termsQuery("fieldName", "fieldlValue1", "fieldlValue2...");
     *      以关键字"开发开放"，关键字支持分词
     *          QueryBuilders.matchQuery("title", "开发开放");
     *          QueryBuilders.multiMatchQuery("fieldlValue", "fieldName1", "fieldName2", "fieldName3");
     *
     *  模糊查询
     *      左右模糊查询，其中fuzziness的参数是再查询时，es动态的将查询关键词前后增加或者删除一个词，然后进行匹配
     *          QueryBuilders.fuzzyQuery("title", "开发开放").fuzziness(Fuzziness.ONE);
     *      前缀查询，查询title中以"开发开放"为前缀的document
     *          QueryBuilders.prefixQuery("title", "开发开放");
     *      通配符查询，支持*和？，？表示单个字符；注意不建议将通配符作为前缀，否则导致查询很慢
     *          QueryBuilders.wildcardQuery("title", "开*放");
     *          QueryBuilders.wildcardQuery("title", "开?放");
     *
     *      注意
     *          在分词的情况下，针对fuzzyQuery、prefixQuery、wildcardQuery不支持分词查询，己式有这种document数据，也不一定能查出来，因为分词后，不一定有"开发开放"这个词；
     *      查询总结
     *          查询关键字               开发开放                                放               开
     *          queryStringQuery    查询目标中含有开发、开放、开发开放的          无               无
     *          matchQuery          同queryStringQuery                           无               无
     *          termQuery           无结果，因为它不支持分词                      无               无
     *          prefixQuery         无结果，因为它不支持分词                      无               有，目标分词中以"开"开头的
     *          fuzzyQuery          无结果，大门时与fuzziness参数有关系           无               无
     *          wildcardQuery       开发开放*无结果                            开*，有            放*，无
     *
     *  范围查询
     *      闭区间查询
     *      QueryBuilders.rangeQuery("fieldName").from("fieldValue1").to("fieldValue2");
     *      开区间查询，默认是true，也就是包含
     *      QueryBuilders.rangeQuery("fieldName").from("fieldValue1").to("fieldValue2").includeUpper(false).includeLower(false);
     *      大于
     *      QueryBuilders.rangeQuery("fieldName").gt("fieldValue");
     *      大于等于
     *      QueryBuilders.rangeQuery("fieldName").gte("fieldValue");
     *      小于
     *      QueryBuilders.rangeQuery("fieldName").lt("fieldValue");
     *      小于等于
     *      QueryBuilders.rangeQuery("fieldName").lte("fieldValue");
     *
     *  多个关键字组合查询boolQuery()
     *      QueryBuilders.boolQuery()
     *      QueryBuilders.boolQuery().must();//文档必须完全匹配条件，相当于and
     *      QueryBuilders.boolQuery().mustNot();//文档必须不匹配条件，相当于not
     *      QueryBuilders.boolQuery().should();//至少满足一个条件，这个文档就符合should，相当于or
     *
     *  SortBuilders排序
     *      按照id字段降序
     *      .withSort(SortBuilders.fieldSort("id").order(SortOrder.DESC))
     *      这里有个坑，在以id排序时，比如降序，结果可能并不是我们想要的，因为根据id排序，es实际上会根据_id进行排序，但是_id是string类型的，排序后的结果会与整型不一致
     *      建议：
     *          在创建es的索引mapping时，将es的id和业务的id分开，比如业务id叫做myId。这样后续排序就可以使用myId
     *
     *  分页
     *      .withPageable(PageRequest.of(0, 50))
     *      如果不指定分页参数，es默认只显示10条
     *
     *  高亮显示
     *      .withHighlightFields(new HighlightBuilder.Field("title").preTags(preTag).postTags(postTag))
     */
    @GetMapping("get")
    public Object get() {
        Criteria criteria = new Criteria().and("stuId").is(1000);

        Query query = new CriteriaQuery(criteria);
        Stu stu = esTemplate.searchOne(query, Stu.class).getContent();
        log.info("查询的stuId为1000数据:[{}]", stu);

        Criteria criteria2 = new Criteria("age").is(22).and("money").greaterThan(500);
        Query query2 = new CriteriaQuery(criteria2);
        SearchHits<Stu> searchHits = esTemplate.search(query2, Stu.class);
        List<SearchHit<Stu>> hits = searchHits.getSearchHits();
        List<Stu> stuList = new ArrayList<>();
        for (SearchHit<Stu> searchHit : hits) {
            Stu res = searchHit.getContent();
            log.info("查询的Stu:[{}]", res);
            stuList.add(res);
        }

        return "OK";
    }

    /**
     * 删除文档
     */
    @GetMapping("delete")
    public Object delete() {
        String deleteResult = esTemplate.delete("1002", IndexCoordinates.of("stu"));
        // 也可以根据查询多条件进行删除
//        Criteria criteria = new Criteria("age").is(22).and("money").greaterThan(500);
//        esTemplate.delete(new CriteriaQuery(criteria));
        log.info("删除返回:[{}]", deleteResult);
        return "OK";
    }

    /**
     * 保存数据
     */
    @PostMapping("saveTestData")
    public Object saveTestData(@RequestParam String userId,
                               @RequestParam String aId,
                               @RequestParam String name,
                               @RequestParam Integer age,
                               @RequestParam String birthday,
                               @RequestParam String refreshTime) {
        Tes123 data = new Tes123();
        data.setUserId(userId);
        data.setAId(aId);
        data.setName(name);
        data.setAge(age);
        data.setBirthday(birthday);
        data.setRefreshTime(refreshTime);

        IndexQuery iq = new IndexQueryBuilder().withObject(data).build();

        IndexCoordinates ic = IndexCoordinates.of("tes123");

        String saveResult = esTemplate.index(iq, ic);
        log.info("新增返回:[{}]", saveResult);
        return "OK";
    }

    @GetMapping("testQuery")
    public Object testQuery() {

        /**
         * 精确查询
         *      指定字符串作为关键词查询，关键词支持分词
         *          查询title字段中，包含“开发”，“开放”这个字符串的document；相当于把“浦东开发开放”分词了，再查询；
         *          QueryBuilders.queryStringQuery("开发开放").defaultField("title");
         *          不指定feild，查询范围为所有feild
         *          QueryBuilders.queryStringQuery("青春");
         *          指定多个feild
         *          QueryBuilders.queryStringQuery("青春").field("title").field("content");
         *      以关键字"开发开放"，关键字不支持分词
         *          QueryBuilders.termQuery("title", "开发开放");
         *          QueryBuilders.termsQuery("fieldName", "fieldlValue1", "fieldlValue2...");
         *      以关键字"开发开放"，关键字支持分词
         *          QueryBuilders.matchQuery("title", "开发开放");
         *          QueryBuilders.multiMatchQuery("fieldlValue", "fieldName1", "fieldName2", "fieldName3");
         */
        // 指定字符串作为关键词查询，关键词支持分词
//        Query query = new NativeSearchQueryBuilder()
//                .withQuery(QueryBuilders.queryStringQuery("测试1").defaultField("name"))
//                .build();
//
//        SearchHits<Tes123> searchHits = esTemplate.search(query, Tes123.class);
//        List<Tes123> list = getList(searchHits, Tes123.class);
//        log.info("测试：{}", list);

//        Query query = new NativeSearchQueryBuilder()
//                .withQuery(QueryBuilders.queryStringQuery("测试"))
//                .build();
//
//        SearchHits<Tes123> searchHits = esTemplate.search(query, Tes123.class);
//        List<Tes123> list = getList(searchHits, Tes123.class);
//        log.info("测试：{}", list);

//        Query query = new NativeSearchQueryBuilder()
//                .withQuery(QueryBuilders.queryStringQuery("10").field("name").field("age"))
//                .build();
//
//        SearchHits<Tes123> searchHits = esTemplate.search(query, Tes123.class);
//        List<Tes123> list = getList(searchHits, Tes123.class);
//        log.info("测试：{}", list);

        // 以关键字""，关键字不支持分词
//        Query query = new NativeSearchQueryBuilder()
//                .withQuery(QueryBuilders.termQuery("name", "测试"))
//                .build();
//
//        SearchHits<Tes123> searchHits = esTemplate.search(query, Tes123.class);
//        List<Tes123> list = getList(searchHits, Tes123.class);
//        log.info("测试：{}", list);

//        Query query = new NativeSearchQueryBuilder()
//                .withQuery(QueryBuilders.termsQuery("name", "测试", "试验"))
//                .build();
//
//        SearchHits<Tes123> searchHits = esTemplate.search(query, Tes123.class);
//        List<Tes123> list = getList(searchHits, Tes123.class);
//        log.info("测试：{}", list);

        // 以关键字""，关键字支持分词
//        Query query = new NativeSearchQueryBuilder()
//                .withQuery(QueryBuilders.matchQuery("name", "测试"))
//                .build();
//
//        SearchHits<Tes123> searchHits = esTemplate.search(query, Tes123.class);
//        List<Tes123> list = getList(searchHits, Tes123.class);
//        log.info("测试：{}", list);

//        Query query = new NativeSearchQueryBuilder()
//                .withQuery(QueryBuilders.multiMatchQuery("1", "name", "age"))
//                .build();
//
//        SearchHits<Tes123> searchHits = esTemplate.search(query, Tes123.class);
//        List<Tes123> list = getList(searchHits, Tes123.class);
//        log.info("测试：{}", list);

        /**
         * 模糊查询
         *      左右模糊查询，其中fuzziness的参数是再查询时，es动态的将查询关键词前后增加或者删除一个词，然后进行匹配
         *          QueryBuilders.fuzzyQuery("title", "开发开放").fuzziness(Fuzziness.ONE);
         *      前缀查询，查询title中以"开发开放"为前缀的document
         *          QueryBuilders.prefixQuery("title", "开发开放");
         *      通配符查询，支持*和？，？表示单个字符；注意不建议将通配符作为前缀，否则导致查询很慢
         *          QueryBuilders.wildcardQuery("title", "开*放");
         *          QueryBuilders.wildcardQuery("title", "开?放");
         */
        // 左右模糊查询，其中fuzziness的参数是再查询时，es动态的将查询关键词前后增加或者删除一个词，然后进行匹配
//        Query query = new NativeSearchQueryBuilder()
//                .withQuery(QueryBuilders.fuzzyQuery("name", "试").fuzziness(Fuzziness.ONE))
//                .build();
//
//        SearchHits<Tes123> searchHits = esTemplate.search(query, Tes123.class);
//        List<Tes123> list = getList(searchHits, Tes123.class);
//        log.info("测试：{}", list);

        // 前缀查询，查询title中以"开发开放"为前缀的document
//        Query query = new NativeSearchQueryBuilder()
//                .withQuery(QueryBuilders.prefixQuery("name", "测"))
//                .build();
//
//        SearchHits<Tes123> searchHits = esTemplate.search(query, Tes123.class);
//        List<Tes123> list = getList(searchHits, Tes123.class);
//        log.info("测试：{}", list);

        // 通配符查询，支持*和？，？表示单个字符；注意不建议将通配符作为前缀，否则导致查询很慢
//        Query query = new NativeSearchQueryBuilder()
//                .withQuery(QueryBuilders.wildcardQuery("name", "测*"))
//                .build();
//
//        SearchHits<Tes123> searchHits = esTemplate.search(query, Tes123.class);
//        List<Tes123> list = getList(searchHits, Tes123.class);
//        log.info("测试：{}", list);

        /**
         *  范围查询
         *      闭区间查询
         *      QueryBuilders.rangeQuery("fieldName").from("fieldValue1").to("fieldValue2");
         *      开区间查询，默认是true，也就是包含
         *      QueryBuilders.rangeQuery("fieldName").from("fieldValue1").to("fieldValue2").includeUpper(false).includeLower(false);
         *      大于
         *      QueryBuilders.rangeQuery("fieldName").gt("fieldValue");
         *      大于等于
         *      QueryBuilders.rangeQuery("fieldName").gte("fieldValue");
         *      小于
         *      QueryBuilders.rangeQuery("fieldName").lt("fieldValue");
         *      小于等于
         *      QueryBuilders.rangeQuery("fieldName").lte("fieldValue");
         */
        // 闭区间查询
//        Query query = new NativeSearchQueryBuilder()
//                .withQuery(QueryBuilders.rangeQuery("age").from(10).to(13))
//                .build();
//
//        SearchHits<Tes123> searchHits = esTemplate.search(query, Tes123.class);
//        List<Tes123> list = getList(searchHits, Tes123.class);
//        log.info("测试：{}", list);

        // 开区间查询，默认是true，也就是包含
//        Query query = new NativeSearchQueryBuilder()
//                .withQuery(QueryBuilders.rangeQuery("age").from(10).to(13).includeUpper(false).includeLower(false))
//                .build();
//
//        SearchHits<Tes123> searchHits = esTemplate.search(query, Tes123.class);
//        List<Tes123> list = getList(searchHits, Tes123.class);
//        log.info("测试：{}", list);

        /**
         *  多个关键字组合查询boolQuery()
         *      QueryBuilders.boolQuery()
         *      QueryBuilders.boolQuery().must();//文档必须完全匹配条件，相当于and
         *      QueryBuilders.boolQuery().mustNot();//文档必须不匹配条件，相当于not
         *      QueryBuilders.boolQuery().should();//至少满足一个条件，这个文档就符合should，相当于or
         */
//        Query query = new NativeSearchQueryBuilder()
//                .withQuery(QueryBuilders.boolQuery()
//                        .must(QueryBuilders.termQuery("name", "测试"))
////                        .must(QueryBuilders.termQuery("age", 11))
//                        .mustNot(QueryBuilders.termQuery("age", 11))
//                ).build();

//        Query query = new NativeSearchQueryBuilder()
//                .withQuery(QueryBuilders.boolQuery()
//                        .should(QueryBuilders.termQuery("name", "测试"))
//                        .should(QueryBuilders.termQuery("age", 18))
//                ).build();
//
//
//
//        SearchHits<Tes123> searchHits = esTemplate.search(query, Tes123.class);
//        List<Tes123> list = getList(searchHits, Tes123.class);
//        log.info("测试：{}", list);

        Query query = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.boolQuery()
                        .must(QueryBuilders.termQuery("username", "用户名称1"))
                ).build();

        SearchHits<Tes12345> searchHits = esTemplate.search(query, Tes12345.class);
        List<Tes12345> list = getList(searchHits, Tes12345.class);
        log.info("测试：{}", list);


        return "OK";
    }

    /**
     * 分页查询
     *  match_all   查询所有
     *      相当于select * from
     *
     *      /index_name/_doc/_search
     *      {
     *          "query": {
     *              "match_all": {}
     *          }
     *      }
     *  term        按照值查询
     *      /index_name/_doc/_search
     *      {
     *          "query": {
     *              "term": {"sex": 1}
     *          }
     *      }
     *
     *      where major like
     *      {
     *          "query": {
     *              "match": {"major": "会计理财"}
     *          }
     *      }
     *      major"金融会计会查询出来"，做分词处理了
     *
     *  title       按照关键字对某一个field进行查询
     */
    @GetMapping("match_all")
    public Object matchAll(Integer page, Integer pageSize) {

        // es的页面是从0开始的，所以此处page需要-1
        if (page < 1) page = 1;
        page--;
        Pageable pageable = PageRequest.of(page, pageSize);

        // NativeSearchQueryBuilder 多条件查询构造器（复杂查询）
        Query query = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.matchAllQuery())
                .withPageable(pageable)
                .build();

        SearchHits<Stu> searchHits = esTemplate.search(query, Stu.class);
        List<Stu> list = getList(searchHits, Stu.class);

        return list;
    }

    /**
     * 多条件查询
     */
    @GetMapping("term")
    public Object term() {

        // NativeSearchQueryBuilder 多条件查询构造器（复杂查询）
        Query query = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.termQuery("age", 29))
                .withQuery(QueryBuilders.matchQuery("name", "wyw"))
                .build();

        SearchHits<Stu> searchHits = esTemplate.search(query, Stu.class);
        List<Stu> list = getList(searchHits, Stu.class);

        return list;
    }

    private <T> List<T> getList(SearchHits<T> searchHits, Class<T> clazz) {
        List<SearchHit<T>> hits = searchHits.getSearchHits();
        List<T> list = new ArrayList<>();
        for (SearchHit<T> searchHit : hits) {
            T res = searchHit.getContent();
            list.add(res);
        }
        return list;
    }


}
